/**
 * This file is Copyright © 2008 Software Craftsmen Limited. All Rights Reserved.
 */
package com.softwarecraftsmen.orogeny;

import com.softwarecraftsmen.orogeny.annotations.InternalApi;
import com.softwarecraftsmen.orogeny.buildEnvironments.BuildDateTime;
import com.softwarecraftsmen.orogeny.buildEnvironments.BuildEnvironment;
import com.softwarecraftsmen.orogeny.buildEnvironments.RegisteredClassPathUnavailableException;
import com.softwarecraftsmen.orogeny.execution.BuildExecuter;
import com.softwarecraftsmen.orogeny.execution.TasksExecuteInParallelProcessor;
import com.softwarecraftsmen.orogeny.filing.AbsoluteDirectory;
import com.softwarecraftsmen.orogeny.filing.AbsoluteFile;
import com.softwarecraftsmen.orogeny.filing.AbsolutePath;
import com.softwarecraftsmen.orogeny.filing.AbsolutePaths;
import com.softwarecraftsmen.orogeny.filing.fileSystems.FileSystem;
import com.softwarecraftsmen.orogeny.filing.findFileFilters.FindFilesFilter;
import com.softwarecraftsmen.orogeny.filing.findSubdirectoriesFilters.FindSubdirectoriesFilter;
import com.softwarecraftsmen.orogeny.operatingSystems.OperatingSystem;
import com.softwarecraftsmen.orogeny.operatingSystems.OperatingSystemFamily;
import com.softwarecraftsmen.orogeny.operatingSystems.OperatingSystemPlatform;
import com.softwarecraftsmen.orogeny.properties.Evaluatable;
import com.softwarecraftsmen.orogeny.properties.ExpressionProperty;
import com.softwarecraftsmen.orogeny.properties.PropertyName;
import com.softwarecraftsmen.orogeny.taskNames.TaskName;
import com.softwarecraftsmen.orogeny.taskNames.TaskName.SomeTaskName;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;

import java.io.File;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Set;
import java.util.UUID;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

import static com.softwarecraftsmen.orogeny.filing.AbsoluteDirectories.EmptyAbsoluteDirectories;
import static com.softwarecraftsmen.orogeny.properties.PropertyExpression.parseExpression;
import static com.softwarecraftsmen.orogeny.properties.PropertyName.propertyName;
import static com.softwarecraftsmen.orogeny.taskNames.TaskName.taskName;
import static java.lang.String.format;
import static java.util.Locale.UK;

public abstract class BuildScript
{
	private final BuildEnvironment ourBuildEnvironment;

	private final Map<TaskName, Task> allTasks;
	private final Map<TaskName, String> tasksToLines;
	private static String ourCurrentLine;
	private Task previousTask;

	private static BuildEnvironment constructorArgumentBuildEnvironment;

	protected BuildScript()
	{
		ourCurrentLine = "Line -1";
		ourBuildEnvironment = constructorArgumentBuildEnvironment;
		tasksToLines = new LinkedHashMap<>();
		allTasks = new LinkedHashMap<TaskName, Task>()
		{
			@Nullable
			public Task put(final @NotNull TaskName taskName, final @NotNull Task task)
			{
				if (containsKey(taskName))
				{
					throw new TaskAlreadyDefinedException(taskName);
				}

				tasksToLines.put(taskName, ourCurrentLine);
				if (previousTask != null)
				{
					previousTask.guardHasDependenciesAndDoesSomething();
				}
				previousTask = task;
				return super.put(taskName, task);
			}
		};
		previousTask = null;
	}

	@InternalApi
	@SuppressWarnings({"UnusedDeclaration"})
	@NotNull
	private static BuildExecuter instantiate(final @NotNull Class<? extends BuildScript> buildScriptClass, final @NotNull BuildEnvironment buildEnvironment)
	{
		final BuildScript buildScript;
		synchronized (BuildScript.class)
		{
			constructorArgumentBuildEnvironment = buildEnvironment;
			try
			{
				buildScript = buildScriptClass.newInstance();
				return new BuildExecuter(buildScript.allTasks, buildEnvironment, new TasksExecuteInParallelProcessor(buildScriptClass));
			}
			catch (InstantiationException e)
			{
				throw new RuntimeException(format(UK, "Could not run script %1$s", buildEnvironment.currentBuildName()), e.getCause());
			}
			catch (IllegalAccessException e)
			{
				throw new RuntimeException(format(UK, "Could not access class %1$s in script %2$s", buildScriptClass.getName(), buildEnvironment.currentBuildName()));
			}
			finally
			{
				constructorArgumentBuildEnvironment = null;
			}
		}
	}

	@SuppressWarnings({"unchecked"})
	protected static void configure()
	{
	}

	@NotNull
	public String currentBuildName()
	{
		return ourBuildEnvironment.currentBuildName();
	}

	@NotNull
	public UUID currentBuildUuid()
	{
		return ourBuildEnvironment.currentBuildUuid();
	}

	@NotNull
	public BuildDateTime currentBuildDateTime()
	{
		return ourBuildEnvironment.currentBuildDateTime();
	}

	@NotNull
	public OperatingSystem currentOperatingSystem()
	{
		return ourBuildEnvironment.currentOperatingSystem();
	}

	@NotNull
	public OperatingSystemPlatform currentOperatingSystemPlatform()
	{
		return ourBuildEnvironment.currentOperatingSystemPlatform();
	}

	@NotNull
	public OperatingSystemFamily currentOperatingSystemFamily()
	{
		return ourBuildEnvironment.currentOperatingSystemFamily();
	}

	@NotNull
	public FileSystem currentFileSystem()
	{
		return ourBuildEnvironment.currentFileSystem();
	}

	@NotNull
	public AbsoluteDirectory currentWorkingDirectory()
	{
		return ourBuildEnvironment.currentWorkingDirectory();
	}

	@NotNull
	public AbsoluteDirectory currentUserHomeDirectory()
	{
		return ourBuildEnvironment.currentUserHomeDirectory();
	}

	@NotNull
	public AbsoluteDirectory inBuildDirectory(final @NotNull String... subFolders)
	{
		return ourBuildEnvironment.currentBuildDirectory().subDirectory(subFolders);
	}

	@NotNull
	public AbsolutePaths registeredPaths(final @NotNull String... names)
	{
		if (names.length == 1)
		{
			return new RegisteredPaths(ourBuildEnvironment, names[0]);
		}

		AbsolutePaths absolutePaths = EmptyAbsoluteDirectories;
		for (String name : names)
		{
			absolutePaths = new RegisteredPaths(ourBuildEnvironment, name).and(absolutePaths);
		}
		return absolutePaths;
	}

	@NotNull
	public PropertyDefinition defineProperty(final @NotNull String unprefixedDynamicPropertyName)
	{
		setCurrentLine();
		final PropertyName propertyName = propertyName(unprefixedDynamicPropertyName);
		if (!propertyName.isDynamic())
		{
			throw new OnlyDynamicPropertiesCanBeDefinedException(propertyName);
		}
		return new PropertyDefinition(propertyName);
	}

	public final class PropertyDefinition
	{
		private final PropertyName propertyName;

		private PropertyDefinition(final @NotNull PropertyName propertyName)
		{
			this.propertyName = propertyName;
		}

		@NotNull
		public PropertyName as(final @NotNull String expression)
		{
			final Evaluatable property = new ExpressionProperty(propertyName, parseExpression(expression));
			ourBuildEnvironment.putProperty(propertyName, property);
			return propertyName;
		}
	}

	@NotNull
	public Task task(final @NotNull String name)
	{
		setCurrentLine();
		final SomeTaskName taskName = taskName(name);
		final Task task = new Task(taskName);
		allTasks.put(taskName, task);
		return task;
	}

	@NotNull
	public PropertyName property(final @NotNull String prefixedOrUnPrefixedPropertyName)
	{
		setCurrentLine();
		return propertyName(prefixedOrUnPrefixedPropertyName);
	}

	@SuppressWarnings({"ThrowableInstanceNeverThrown"})
	private void setCurrentLine()
	{
		final StackTraceElement line = new RuntimeException().fillInStackTrace().getStackTrace()[2];
		ourCurrentLine = format(UK, "%1$s, line %2$s", line.getFileName(), line.getLineNumber());
	}

	public final class TaskAlreadyDefinedException extends BuildSyntaxException
	{
		public TaskAlreadyDefinedException(final @NotNull TaskName taskName)
		{
			super(format(UK, "Task %1$s previously defined at line %2$s", taskName, tasksToLines.get(taskName)));
		}
	}

	public final class OnlyDynamicPropertiesCanBeDefinedException extends BuildSyntaxException
	{
		public OnlyDynamicPropertiesCanBeDefinedException(final @NotNull PropertyName propertyName)
		{
			super(format(UK, "Only dynamic properties can be defined. (%1$s) is not dynamic", propertyName));
		}
	}

	public static class BuildSyntaxException extends RuntimeException
	{
		public BuildSyntaxException(final @NotNull String message)
		{
			super(format(UK, "%1$s: %2$s", ourCurrentLine, message));
		}
	}

	private static class RegisteredPaths implements AbsolutePaths
	{
		private final BuildEnvironment ourBuildEnvironment;
		private final String name;

		protected volatile AbsolutePaths evaluate;
		protected final Lock evaluateLock;

		private volatile String toFileSystemSpecificPath;
		private final Lock toFileSystemSpecificPathLock;

		private volatile Set<File> toFiles;
		private final Lock toFilesLock;

		private volatile Set<AbsoluteFile> files;
		private final Lock filesLock;

		private volatile Set<AbsoluteDirectory> directories;
		private final Lock directoriesLock;

		private volatile Boolean exists;
		private final Lock existsLock;

		private volatile Boolean isEmpty;
		private final Lock isEmptyLock;

		public RegisteredPaths(final @NotNull BuildEnvironment ourBuildEnvironment, final @NotNull String name)
		{
			this.ourBuildEnvironment = ourBuildEnvironment;
			this.name = name;

			evaluate = null;
			evaluateLock = new ReentrantLock(false);

			toFileSystemSpecificPath = null;
			toFileSystemSpecificPathLock = new ReentrantLock(false);

			toFiles = null;
			toFilesLock = new ReentrantLock(false);

			files = null;
			filesLock = new ReentrantLock(false);

			directories = null;
			directoriesLock = new ReentrantLock(false);

			exists = null;
			existsLock = new ReentrantLock(false);

			isEmpty = null;
			isEmptyLock = new ReentrantLock(false);
		}

		@NotNull
		public String toString()
		{
			try
			{
				return toFileSystemSpecificPath();
			}
			catch(RegisteredClassPathUnavailableException notYetAvailable)
			{
				return format(UK, "RegisteredPaths(%1$s)", name);
			}
		}

		public boolean equals(final @Nullable Object o)
		{
			if (this == o)
			{
				return true;
			}
			if (o == null || getClass() != o.getClass())
			{
				return false;
			}

			final RegisteredPaths that = (RegisteredPaths) o;
			return name.equals(that.name) && ourBuildEnvironment.equals(that.ourBuildEnvironment);
		}

		public int hashCode()
		{
			int result;
			result = ourBuildEnvironment.hashCode();
			result = 31 * result + name.hashCode();
			return result;
		}

		@NotNull
		protected AbsolutePaths evaluate()
		{
			if (this.evaluate != null)
			{
				return this.evaluate;
			}
			evaluateLock.lock();
			try
			{
				this.evaluate = ourBuildEnvironment.registeredPaths(name);
				return this.evaluate;
			}
			catch(BuildSyntaxException e)
			{
				e.fillInStackTrace();
				throw e;
			}
			finally
			{
				evaluateLock.unlock();
			}
		}

		@NotNull
		public AbsolutePaths and(final @NotNull AbsolutePath absolutePath)
		{
			final class RegisteredPathsAndAbsolutePath extends RegisteredPaths
			{
				private final RegisteredPaths parent = RegisteredPaths.this;
				private final AbsolutePath andAbsolutePath = absolutePath;

				public RegisteredPathsAndAbsolutePath(final @NotNull BuildEnvironment ourBuildEnvironment)
				{
					super(ourBuildEnvironment, "IRRELEVANT");
				}

				@NotNull
				protected AbsolutePaths evaluate()
				{
					if (this.evaluate != null)
					{
						return this.evaluate;
					}
					evaluateLock.lock();
					try
					{
						this.evaluate = parent.evaluate().and(andAbsolutePath);
						return this.evaluate;
					}
					catch (BuildSyntaxException e)
					{
						e.fillInStackTrace();
						throw e;
					}
					finally
					{
						evaluateLock.unlock();
					}
				}

				public boolean equals(final @Nullable Object o)
				{
					if (this == o)
					{
						return true;
					}
					if (o == null || getClass() != o.getClass())
					{
						return false;
					}
					if (!super.equals(o))
					{
						return false;
					}

					final RegisteredPathsAndAbsolutePath that = (RegisteredPathsAndAbsolutePath) o;
					return andAbsolutePath.equals(that.andAbsolutePath) && parent.equals(that.parent);
				}

				public int hashCode()
				{
					int result = super.hashCode();
					result = 31 * result + parent.hashCode();
					result = 31 * result + andAbsolutePath.hashCode();
					return result;
				}
			}
			return new RegisteredPathsAndAbsolutePath(ourBuildEnvironment);
		}

		@NotNull
		public AbsolutePaths and(final @NotNull AbsolutePaths absolutePaths)
		{
			final class RegisteredPathsAndAbsolutePaths extends RegisteredPaths
			{
				private final RegisteredPaths parent = RegisteredPaths.this;
				private final AbsolutePaths andAbsolutePaths = absolutePaths;

				public RegisteredPathsAndAbsolutePaths(final @NotNull BuildEnvironment ourBuildEnvironment)
				{
					super(ourBuildEnvironment, "IRRELEVANT");
				}

				@NotNull
				protected AbsolutePaths evaluate()
				{
					if (this.evaluate != null)
					{
						return this.evaluate;
					}
					evaluateLock.lock();
					try
					{
						this.evaluate = parent.evaluate().and(andAbsolutePaths);
						return this.evaluate;
					}
					catch (BuildSyntaxException e)
					{
						e.fillInStackTrace();
						throw e;
					}
					finally
					{
						evaluateLock.unlock();
					}
				}

				public boolean equals(final @Nullable Object o)
				{
					if (this == o)
					{
						return true;
					}
					if (o == null || getClass() != o.getClass())
					{
						return false;
					}
					if (!super.equals(o))
					{
						return false;
					}

					final RegisteredPathsAndAbsolutePaths that = (RegisteredPathsAndAbsolutePaths) o;
					return andAbsolutePaths.equals(that.andAbsolutePaths) && parent.equals(that.parent);
				}

				public int hashCode()
				{
					int result = super.hashCode();
					result = 31 * result + parent.hashCode();
					result = 31 * result + andAbsolutePaths.hashCode();
					return result;
				}
			}
			return new RegisteredPathsAndAbsolutePaths(ourBuildEnvironment);
		}

		@NotNull
		public String toFileSystemSpecificPath()
		{
			// No point using java.util.concurrent.atomic.AtomicReferenceFieldUpdater.compareAndSet as we'll always need to undertake the long running operation
			if (toFileSystemSpecificPath != null)
			{
				return toFileSystemSpecificPath;
			}

			toFileSystemSpecificPathLock.lock();
			try
			{
				toFileSystemSpecificPath = evaluate().toFileSystemSpecificPath();
			}
			finally
			{
				toFileSystemSpecificPathLock.unlock();
			}
			return toFileSystemSpecificPath;
		}

		@NotNull
		public Set<File> toFiles()
		{
			if (toFiles != null)
			{
				return toFiles;
			}

			toFilesLock.lock();
			try
			{
				toFiles = evaluate().toFiles();
			}
			finally
			{
				toFilesLock.unlock();
			}
			return toFiles;
		}

		@NotNull
		public Set<AbsoluteFile> files()
		{
			if (files != null)
			{
				return files;
			}

			filesLock.lock();
			try
			{
				files = evaluate().files();
			}
			finally
			{
				filesLock.unlock();
			}
			return files;
		}

		@NotNull
		public Set<AbsoluteDirectory> directories()
		{
			if (directories != null)
			{
				return directories;
			}

			directoriesLock.lock();
			try
			{
				directories = evaluate().directories();
			}
			finally
			{
				directoriesLock.unlock();
			}
			return directories;
		}

		public boolean exists()
		{
			if (exists != null)
			{
				return exists;
			}

			existsLock.lock();
			try
			{
				exists = evaluate().exists();
			}
			finally
			{
				existsLock.unlock();
			}
			return exists;
		}

		public boolean isEmpty()
		{
			if (isEmpty != null)
			{
				return isEmpty;
			}

			isEmptyLock.lock();
			try
			{
				isEmpty = evaluate().isEmpty();
			}
			finally
			{
				isEmptyLock.unlock();
			}
			return isEmpty;
		}

		@NotNull
		public Set<AbsoluteFile> findFiles(final @NotNull FindFilesFilter findFilesFilter)
		{
			return evaluate().findFiles(findFilesFilter);
		}

		@NotNull
		public Set<AbsoluteDirectory> findSubdirectories(final @NotNull FindSubdirectoriesFilter findSubdirectoriesFilter)
		{
			return evaluate().findSubdirectories(findSubdirectoriesFilter);
		}
	}
}
